/*! ******************************************************************************
 *
 * Pentaho
 *
 * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file.
 *
 * Change Date: 2029-07-20
 ******************************************************************************/


package org.pentaho.platform.util.client;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.pentaho.commons.util.repository.GetCheckedoutDocsResponse;
import org.pentaho.commons.util.repository.INavigationService;
import org.pentaho.commons.util.repository.exception.ConstraintViolationException;
import org.pentaho.commons.util.repository.exception.FilterNotValidException;
import org.pentaho.commons.util.repository.exception.FolderNotValidException;
import org.pentaho.commons.util.repository.exception.InvalidArgumentException;
import org.pentaho.commons.util.repository.exception.ObjectNotFoundException;
import org.pentaho.commons.util.repository.exception.OperationNotSupportedException;
import org.pentaho.commons.util.repository.exception.PermissionDeniedException;
import org.pentaho.commons.util.repository.exception.RuntimeException;
import org.pentaho.commons.util.repository.exception.UpdateConflictException;
import org.pentaho.commons.util.repository.type.CmisObject;
import org.pentaho.commons.util.repository.type.CmisObjectImpl;
import org.pentaho.commons.util.repository.type.CmisProperties;
import org.pentaho.commons.util.repository.type.CmisProperty;
import org.pentaho.commons.util.repository.type.PropertiesBase;
import org.pentaho.commons.util.repository.type.PropertiesDocument;
import org.pentaho.commons.util.repository.type.PropertyBoolean;
import org.pentaho.commons.util.repository.type.PropertyDateTime;
import org.pentaho.commons.util.repository.type.PropertyId;
import org.pentaho.commons.util.repository.type.PropertyString;
import org.pentaho.commons.util.repository.type.TypesOfFileableObjects;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.util.StringUtil;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.StringTokenizer;

public class BiPlatformRepositoryClientNavigationService implements INavigationService {

  private Document doc;

  public Document getDoc() {
    return doc;
  }

  public void setDoc( Document doc ) {
    this.doc = doc;
  }

  public List<CmisObject> getObjectParent( String repositoryId, String objectId, String filter,
      boolean includeAllowableActions, boolean includeRelationships ) throws InvalidArgumentException,
    ConstraintViolationException, FilterNotValidException, RuntimeException, UpdateConflictException,
    ObjectNotFoundException, OperationNotSupportedException, PermissionDeniedException, FolderNotValidException {

    // TODO add support for filters

    if ( !repositoryId.equals( BiPlatformRepositoryClient.PLATFORMORIG ) ) {
      throw new InvalidArgumentException();
    }

    List<CmisObject> objects = new ArrayList<CmisObject>();

    // get the element for the specified object
    Element objectElement = getObjectElement( objectId );
    if ( objectElement == null ) {
      return objects;
    }

    // get the parent node
    Element parentElement = objectElement.getParent();
    if ( parentElement == null ) {
      return objects;
    }

    CmisObject parent = createCmisObjectFromElement( parentElement, 0 );

    objects.add( parent );

    return objects;

  }

  private CmisObject createCmisObjectFromElement( Element element, int depth ) {

    CmisObject object = new CmisObjectImpl();
    CmisProperties properties = new CmisProperties();
    List<CmisProperty> propList = properties.getProperties();

    // is this a folder or a file?
    boolean isDirectory = false;
    Attribute attr = element.attribute( "isDirectory" ); //$NON-NLS-1$
    if ( attr != null ) {
      isDirectory = "true".equalsIgnoreCase( attr.getText() ); //$NON-NLS-1$
    }
    // set the base properties
    String objectId = getObjectId( element );
    Calendar lastModifiedDate = getLastModifiedDate( element );
    String name = getName( element );
    String localizedName = getLocalizedName( element );
    String extension = getExtension( element );
    boolean visible = getVisible( element );

    propList.add( new PropertyId( PropertiesBase.OBJECTID, objectId ) );
    propList.add( new PropertyDateTime( PropertiesBase.LASTMODIFICATIONDATE, lastModifiedDate ) );

    if ( isDirectory ) {
      propList.add( new PropertyString( PropertiesBase.OBJECTTYPEID, CmisObject.OBJECT_TYPE_FOLDER ) );
    } else {
      propList.add( new PropertyString( PropertiesBase.OBJECTTYPEID, extension ) );
      propList.add( new PropertyBoolean( PropertiesDocument.CONTENTSTREAMALLOWED, true ) );
    }

    propList.add( new PropertyString( CmisObject.NAME, name ) );
    propList.add( new PropertyString( CmisObject.LOCALIZEDNAME, localizedName ) );
    propList.add( new PropertyBoolean( CmisObject.VISIBLE, visible ) );

    object.setProperties( properties );
    return object;
  }

  private String getObjectId( Element element ) {

    // get a list of the ancestors
    List<Element> pathElements = new ArrayList<Element>();
    Element current = element;
    while ( current != null ) {
      pathElements.add( 0, current );
      current = current.getParent();
    }

    // now create a path string
    StringBuilder sb = new StringBuilder();
    int idx = 0;
    for ( Element pathElement : pathElements ) {
      if ( idx > 1 ) {
        sb.append( RepositoryFile.SEPARATOR );
      }
      if ( idx > 0 ) {
        // ignore the first element
        sb.append( pathElement.attribute( "name" ).getText() ); //$NON-NLS-1$
      }
      idx++;
    }
    String objectId = sb.toString();
    return objectId;

  }

  private Calendar getLastModifiedDate( Element element ) {
    String lastModifiedDateStr = ""; //$NON-NLS-1$
    Attribute attr = element.attribute( "lastModifiedDate" ); //$NON-NLS-1$
    if ( attr != null ) {
      lastModifiedDateStr = attr.getText();
      long millis = new Long( lastModifiedDateStr );
      Calendar calendar = Calendar.getInstance();
      calendar.setTimeInMillis( millis );
      return calendar;
    }
    return null;
  }

  private String getLocalizedName( Element element ) {
    String name = ""; //$NON-NLS-1$
    Attribute attr = element.attribute( "localized-name" ); //$NON-NLS-1$
    if ( attr != null ) {
      name = attr.getText();
    }
    return name;
  }

  private String getName( Element element ) {
    String name = ""; //$NON-NLS-1$
    Attribute attr = element.attribute( "name" ); //$NON-NLS-1$
    if ( attr != null ) {
      name = attr.getText();
    }
    return name;
  }

  private boolean getVisible( Element element ) {
    boolean visible = true;
    Attribute attr = element.attribute( "visible" ); //$NON-NLS-1$
    if ( attr != null ) {
      visible = "true".equalsIgnoreCase( attr.getText() ); //$NON-NLS-1$
    }
    return visible;
  }

  private String getExtension( Element element ) {
    String name = ""; //$NON-NLS-1$
    Attribute attr = element.attribute( "name" ); //$NON-NLS-1$
    if ( attr != null ) {
      name = attr.getText();
    }
    int idx = name.indexOf( '.' );
    String extension = ""; //$NON-NLS-1$
    if ( idx != -1 ) {
      extension = name.substring( idx + 1 );
    }
    return extension;
  }

  private Element getObjectElement( String path ) throws ObjectNotFoundException {
    // parse out the path
    StringTokenizer tokenizer = new StringTokenizer( path, RepositoryFile.SEPARATOR ); //$NON-NLS-1$

    StringBuilder sb = new StringBuilder();
    sb.append( "/repository" ); //$NON-NLS-1$
    String folderName;
    int tokenCount = tokenizer.countTokens();
    for ( int idx = 0; idx < tokenCount - 1; idx++ ) {
      folderName = tokenizer.nextToken();
      sb.append( "/file[@isDirectory='true' and @name='" ) //$NON-NLS-1$
          .append( folderName ).append( "']" ); //$NON-NLS-1$
    }
    if ( tokenizer.hasMoreTokens() ) {
      folderName = tokenizer.nextToken();
      sb.append( "/file[@name='" ) //$NON-NLS-1$
          .append( folderName ).append( "']" ); //$NON-NLS-1$
    }
    String xPath = sb.toString();
    Element element = (Element) doc.selectSingleNode( xPath );
    if ( element == null ) {
      throw new ObjectNotFoundException();
    }
    return element;

  }

  private Element getFolderElement( String path ) throws FolderNotValidException {

    // parse out the path
    StringTokenizer tokenizer = new StringTokenizer( path, "" + RepositoryFile.SEPARATOR ); //$NON-NLS-1$

    StringBuilder sb = new StringBuilder();
    sb.append( "/repository" ); //$NON-NLS-1$
    String folderName;
    while ( tokenizer.hasMoreTokens() ) {
      folderName = tokenizer.nextToken();
      sb.append( "/file[@isDirectory='true' and @name='" ) //$NON-NLS-1$
          .append( folderName ).append( "']" ); //$NON-NLS-1$
    }
    String xPath = sb.toString();
    Element element = (Element) doc.selectSingleNode( xPath );
    if ( element == null ) {
      throw new FolderNotValidException();
    }
    return element;
  }

  public List<CmisObject> getFolderParent( String repositoryId, String folderId, String filter,
      boolean includeAllowableActions, boolean includeRelationships, boolean returnToRoot )
    throws InvalidArgumentException, ConstraintViolationException, FilterNotValidException, RuntimeException,
    UpdateConflictException, ObjectNotFoundException, OperationNotSupportedException, PermissionDeniedException,
    FolderNotValidException {

    // TODO add support for filters

    if ( !repositoryId.equals( BiPlatformRepositoryClient.PLATFORMORIG ) ) {
      throw new InvalidArgumentException();
    }

    List<CmisObject> objects = new ArrayList<CmisObject>();

    // get the element for the specified object
    Element objectElement = getFolderElement( folderId );

    // get the parent node
    Element parentElement = objectElement.getParent();

    if ( parentElement == null ) {
      return objects;
    }
    CmisObject parent = createCmisObjectFromElement( parentElement, 0 );

    objects.add( parent );

    return objects;
  }

  public List<CmisObject> getDescendants( String repositoryId, String folderId, TypesOfFileableObjects type, int depth,
      String filter, boolean includeAllowableActions, boolean includeRelationships ) throws InvalidArgumentException,
    ConstraintViolationException, FilterNotValidException, RuntimeException, UpdateConflictException,
    ObjectNotFoundException, OperationNotSupportedException, PermissionDeniedException, FolderNotValidException {

    Collection filters = null;
    if ( !StringUtil.isEmpty( filter ) ) {
      filters = getFilterCollection( filter );
    }

    if ( !repositoryId.equals( BiPlatformRepositoryClient.PLATFORMORIG ) ) {
      throw new InvalidArgumentException();
    }

    List<CmisObject> objects = new ArrayList<CmisObject>();

    // get the element for the specified object
    Element objectElement = getFolderElement( folderId );

    addChildren( objects, objectElement, type, filters, 0, 0, depth, 1 );

    return objects;
  }

  protected void addChildren( List<CmisObject> objects, Element objectElement, TypesOfFileableObjects type,
      Collection filters, int maxItems, int skipCount, int depth, int level ) {
    if ( objectElement == null ) {
      return;
    }
    CmisObject object;
    int skipped = 0;
    boolean ok = false;
    for ( Object element : objectElement.elements() ) {

      ok = false;
      object = createCmisObjectFromElement( (Element) element, 0 );
      if ( type == null || type.getValue().equals( TypesOfFileableObjects.ANY ) ) {
        ok = true;
      } else if ( TypesOfFileableObjects.FOLDERS.equals( type.getValue() )
          && CmisObject.OBJECT_TYPE_FOLDER.equals( object.findStringProperty( PropertiesBase.OBJECTTYPEID, null ) ) ) {
        ok = true;
      } else if ( TypesOfFileableObjects.DOCUMENTS.equals( type.getValue() )
          && !CmisObject.OBJECT_TYPE_FOLDER.equals( object.findStringProperty( PropertiesBase.OBJECTTYPEID, null ) ) ) {
        // TODO support policies
        ok = true;
      }
      if ( ok && filters != null ) {
        String objectType = object.findStringProperty( PropertiesBase.OBJECTTYPEID, null );
        if ( !filters.contains( objectType ) ) {
          ok = false;
        }
      }

      if ( ok && skipCount > 0 ) {
        if ( skipped < skipCount ) {
          ok = false;
          skipped++;
        }
      }

      if ( ok && maxItems > 0 ) {
        if ( objects.size() >= maxItems ) {
          break;
        }
      }

      if ( ok ) {
        objects.add( object );
        // see if we have to recurse
      }
      if ( depth > 0 && level < depth
          && CmisObject.OBJECT_TYPE_FOLDER.equals( object.findStringProperty( PropertiesBase.OBJECTTYPEID, null ) ) ) {

        addChildren( objects, (Element) element, type, filters, maxItems, skipCount, depth, level + 1 );

      }
    }
  }

  public List<CmisObject> getChildren( String repositoryId, String folderId, TypesOfFileableObjects type,
      String filter, boolean includeAllowableActions, boolean includeRelationships, int maxItems, int skipCount )
    throws InvalidArgumentException, ConstraintViolationException, FilterNotValidException, RuntimeException,
    UpdateConflictException, ObjectNotFoundException, OperationNotSupportedException, PermissionDeniedException,
    FolderNotValidException {

    Collection filters = null;
    if ( !StringUtil.isEmpty( filter ) ) {
      filters = getFilterCollection( filter );
    }

    if ( !repositoryId.equals( BiPlatformRepositoryClient.PLATFORMORIG ) ) {
      throw new InvalidArgumentException();
    }

    List<CmisObject> objects = new ArrayList<CmisObject>();

    // get the element for the specified object
    Element objectElement = getFolderElement( folderId );

    addChildren( objects, objectElement, type, filters, maxItems, skipCount, 1, 1 );

    return objects;
  }

  private Collection getFilterCollection( String filterstr ) {
    StringTokenizer tokenizer = new StringTokenizer( filterstr, "," ); //$NON-NLS-1$

    HashSet<String> set = new HashSet<String>();
    int tokenCount = tokenizer.countTokens();
    for ( int idx = 0; idx < tokenCount; idx++ ) {
      set.add( tokenizer.nextToken() );
    }
    return set;
  }

  public GetCheckedoutDocsResponse getCheckedoutDocs( String repositoryId, String folderId, String filter,
      boolean includeAllowableActions, boolean includeRelationships, int maxItems, int skipCount )
    throws InvalidArgumentException, ConstraintViolationException, FilterNotValidException, RuntimeException,
    UpdateConflictException, ObjectNotFoundException, OperationNotSupportedException, PermissionDeniedException,
    FolderNotValidException {

    throw new OperationNotSupportedException();

  }

  public String getRepositoryPath( CmisObject object ) {

    // the id is the path and file name
    String id = object.findIdProperty( PropertiesBase.OBJECTID, null );
    String typeId = object.findStringProperty( PropertiesBase.OBJECTTYPEID, null );
    if ( CmisObject.OBJECT_TYPE_FOLDER.equals( typeId ) ) {
      return id;
    } else {
      int idx = id.lastIndexOf( RepositoryFile.SEPARATOR );
      if ( idx != -1 ) {
        return id.substring( 0, idx );
      } else {
        return ""; //$NON-NLS-1$
      }
    }
  }

  public String getRepositoryFilename( CmisObject object ) {

    String typeId = object.findStringProperty( PropertiesBase.OBJECTTYPEID, null );
    if ( CmisObject.OBJECT_TYPE_FOLDER.equals( typeId ) ) {
      return ""; //$NON-NLS-1$
    } else {
      String name = object.findStringProperty( CmisObject.NAME, null );
      return name;
    }
  }

}
